Segundo Semestre - 2021
%%capture
!pip install scikit-optimize
!pip install plotly-express
!pip install jupyter-dash
!pip install jyquickhelper
from jyquickhelper import add_notebook_menu
add_notebook_menu(menu_id="main_menu", last_level=6)
Las enfermedades cardiovasculares son la principal causa de muerte en el mundo, y se calcula que cobran 17,9 millones de vidas al año (OMS). La enfermedad de las arterias coronarias es el tipo más común de enfermedad cardíaca y se produce debido a las obstrucciones (placa) desarrolladas en el interior de las arterias coronarias (vasos sanguíneos que alimentan los músculos del corazón). Los cardiólogos utilizan diversas técnicas de imagen y mediciones invasivas de la presión arterial para examinar y controlar la gravedad de dichas obstrucciones.
Los factores de riesgo conductuales más importantes de estas enfermedades son una dieta poco saludable, la inactividad física, el consumo de tabaco y el uso nocivo del alcohol. Los efectos de los factores de riesgo pueden manifestarse en las personas en forma de aumento de la presión arterial, aumento de la glucosa en sangre, aumento de los lípidos en sangre y sobrepeso y obesidad.
Identificar a las personas con mayor riesgo de sufrir enfermedades cardiovasculares y garantizar que reciban el tratamiento adecuado puede evitar muertes prematuras. Con este objetivo en mente, se quiere utilizar las técnicas de machine learning para construir un modelo que permita predecir qué pacientes pueden estar en riesgo de padecer este tipo de cardiopatía.
Referencias.
OMS (s.f.). “Cardiovascular diseases”.
Fuente de Datos.
Kaggle
Con el objetivo de tener un mayor entendimiento de la información que se tiene para el correspondiente análisis, se realizarán una serie de etapas que nos ayudarán con este proceso. Para este caso se realizará:
# definición de variables
var_sex = {0: "female", 1: "male"}
var_cp = {
1: "typical type 1",
2: "typical type angina",
3: "non angina pain",
4: "asymptomatic",
}
var_fbs = {0: "<120mg/dL", 1: ">120mg/dL"}
var_restecg = {
0: "normal",
1: "having ST-T wave abnormal",
2: "left ventricular hypertrophy",
}
var_exang = {0: "no", 1: "yes"}
var_slope = {1: "unsloping", 2: "flat", 3: "downsloping"}
var_thal = {3: "normal", 6: "fixed", 7: "reversible defect"}
# Importar librerias
import os
import warnings
import numpy as np
import pandas as pd
import plotly.express as px
# Librerias procesamiento de datos
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.preprocessing import KBinsDiscretizer
from sklearn.model_selection import train_test_split
# librerias para el entrenamiento y validación del modelo
from sklearn.ensemble import AdaBoostClassifier, GradientBoostingClassifier
from sklearn.model_selection import cross_val_score
# Librerias para la optimización de hiperparámetros (enfoque bayesiano)
from skopt import gp_minimize
from skopt import BayesSearchCV
from skopt.utils import use_named_args
from skopt.space import Real, Integer, Categorical
# Librerias para la validación del modelo
from sklearn.metrics import accuracy_score
from sklearn.metrics import confusion_matrix
from sklearn.metrics import classification_report
# configuración directorio base
BASE_PATH = '/mnt/f/Projects/ML_Techniques'
os.chdir(BASE_PATH)
# configuración adicional pandas
pd.options.plotting.backend = "plotly"
pd.options.display.max_columns = 100
# configuración warnings
warnings.filterwarnings('ignore')
df = pd.read_csv('data/Labs/lab1/data.csv')
# shape del dataset
df.shape
(303, 15)
# primeras filas
df.head()
| Unnamed: 0 | age | sex | cp | trestbps | chol | fbs | restecg | thalach | exang | oldpeak | slope | ca | thal | class | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 63.0 | 1.0 | 1.0 | 145.0 | 233.0 | 1.0 | 2.0 | 150.0 | 0.0 | 2.3 | 3.0 | 0.0 | 6.0 | 0 |
| 1 | 1 | 67.0 | 1.0 | 4.0 | 160.0 | 286.0 | 0.0 | 2.0 | 108.0 | 1.0 | 1.5 | 2.0 | 3.0 | 3.0 | 2 |
| 2 | 2 | 67.0 | 1.0 | 4.0 | 120.0 | 229.0 | 0.0 | 2.0 | 129.0 | 1.0 | 2.6 | 2.0 | 2.0 | 7.0 | 1 |
| 3 | 3 | 37.0 | 1.0 | 3.0 | 130.0 | 250.0 | 0.0 | 0.0 | 187.0 | 0.0 | 3.5 | 3.0 | 0.0 | 3.0 | 0 |
| 4 | 4 | 41.0 | 0.0 | 2.0 | 130.0 | 204.0 | 0.0 | 2.0 | 172.0 | 0.0 | 1.4 | 1.0 | 0.0 | 3.0 | 0 |
# eliminar columnas no necesarios
df = df.drop('Unnamed: 0', axis=1)
# validación tipos de datos
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 303 entries, 0 to 302 Data columns (total 14 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 age 303 non-null float64 1 sex 303 non-null float64 2 cp 303 non-null float64 3 trestbps 303 non-null float64 4 chol 303 non-null float64 5 fbs 303 non-null float64 6 restecg 303 non-null float64 7 thalach 303 non-null float64 8 exang 303 non-null float64 9 oldpeak 303 non-null float64 10 slope 303 non-null float64 11 ca 303 non-null object 12 thal 303 non-null object 13 class 303 non-null int64 dtypes: float64(11), int64(1), object(2) memory usage: 33.3+ KB
# descripción del dataset
df.describe()
| age | sex | cp | trestbps | chol | fbs | restecg | thalach | exang | oldpeak | slope | class | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 303.000000 | 303.000000 | 303.000000 | 303.000000 | 303.000000 | 303.000000 | 303.000000 | 303.000000 | 303.000000 | 303.000000 | 303.000000 | 303.000000 |
| mean | 54.438944 | 0.679868 | 3.158416 | 131.689769 | 246.693069 | 0.148515 | 0.990099 | 149.607261 | 0.326733 | 1.039604 | 1.600660 | 0.937294 |
| std | 9.038662 | 0.467299 | 0.960126 | 17.599748 | 51.776918 | 0.356198 | 0.994971 | 22.875003 | 0.469794 | 1.161075 | 0.616226 | 1.228536 |
| min | 29.000000 | 0.000000 | 1.000000 | 94.000000 | 126.000000 | 0.000000 | 0.000000 | 71.000000 | 0.000000 | 0.000000 | 1.000000 | 0.000000 |
| 25% | 48.000000 | 0.000000 | 3.000000 | 120.000000 | 211.000000 | 0.000000 | 0.000000 | 133.500000 | 0.000000 | 0.000000 | 1.000000 | 0.000000 |
| 50% | 56.000000 | 1.000000 | 3.000000 | 130.000000 | 241.000000 | 0.000000 | 1.000000 | 153.000000 | 0.000000 | 0.800000 | 2.000000 | 0.000000 |
| 75% | 61.000000 | 1.000000 | 4.000000 | 140.000000 | 275.000000 | 0.000000 | 2.000000 | 166.000000 | 1.000000 | 1.600000 | 2.000000 | 2.000000 |
| max | 77.000000 | 1.000000 | 4.000000 | 200.000000 | 564.000000 | 1.000000 | 2.000000 | 202.000000 | 1.000000 | 6.200000 | 3.000000 | 4.000000 |
Con base en las métricas iniciales, se observa que los datos no presentan valores nulos, sin embargo se observa:
ca, thal tienen un tipo de dato object, pero según el diccionario de datos debería ser de tipo int.Por tal motivo se va a hacer el cambio de datos y validar nuevamente la presencia de valores nulos o errores.
# cast de valores
df.thal = pd.to_numeric(df.thal, errors='coerce')
df.ca = pd.to_numeric(df.ca, errors='coerce')
# validación de types y tamaño del dataset
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 303 entries, 0 to 302 Data columns (total 14 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 age 303 non-null float64 1 sex 303 non-null float64 2 cp 303 non-null float64 3 trestbps 303 non-null float64 4 chol 303 non-null float64 5 fbs 303 non-null float64 6 restecg 303 non-null float64 7 thalach 303 non-null float64 8 exang 303 non-null float64 9 oldpeak 303 non-null float64 10 slope 303 non-null float64 11 ca 299 non-null float64 12 thal 301 non-null float64 13 class 303 non-null int64 dtypes: float64(13), int64(1) memory usage: 33.3 KB
# eliminar valores faltantes
df = df.dropna()
# Definición de columnas a parsear
change_to_int = ['age', 'sex', 'cp', 'trestbps', 'chol', 'fbs', 'restecg', 'thalach', 'exang', 'slope', 'ca', 'thal', 'class']
change_to_cat = ['sex', 'cp', 'fbs', 'restecg', 'exang', 'ca', 'slope', 'thal']
change_to_float = ['oldpeak']
df[change_to_int] = df[change_to_int].astype("int16", errors='ignore')
df[change_to_cat] = df[change_to_cat].astype('category', errors='ignore')
df[change_to_float] = df[change_to_float].astype('float16', errors='ignore')
df.info()
<class 'pandas.core.frame.DataFrame'> Int64Index: 297 entries, 0 to 301 Data columns (total 14 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 age 297 non-null int16 1 sex 297 non-null category 2 cp 297 non-null category 3 trestbps 297 non-null int16 4 chol 297 non-null int16 5 fbs 297 non-null category 6 restecg 297 non-null category 7 thalach 297 non-null int16 8 exang 297 non-null category 9 oldpeak 297 non-null float16 10 slope 297 non-null category 11 ca 297 non-null category 12 thal 297 non-null category 13 class 297 non-null int16 dtypes: category(8), float16(1), int16(5) memory usage: 9.3 KB
Con base en el proceso anterior se evidencia:
ca y thal eran de tipo object debido a que tenían datos incompletos, marcados como ?, sin embargo al realizar el coerce estos cambian como NA, por lo tanto debido a que son datos categóricos, se hace drop de ellos, en vez de rellenarlos.# Función para agregar pacientes por categoría y variable
def gen_class_plot(df: pd.DataFrame, var: str, var_type: dict):
df_var = df[[var, "class"]]
df_var[var] = df[var].map(var_type)
df_var = df_var.groupby([var, "class"]).agg({"class": "count"}).reset_index(level=0)
df_var.index.name = "idx_class"
df_var = df_var.reset_index()
return df_var
Análisis comportamiento de edad
# Análisis comportamiento de edad
fig = px.histogram(df.sort_values('class', ascending=True), x="age", color="class", opacity=0.7, histnorm='probability')
fig.update_layout(title='Distribución - Edades por categoría', barmode='stack')
fig.show()
Presión arterial en reposo
# Presión arterial en reposo
fig = px.histogram(df.sort_values('class', ascending=True), x="trestbps", color="class", opacity=0.7, histnorm='probability')
fig.update_layout(title='Distribución - Presión arterial en reposo por categoría', barmode='stack')
fig.show()
Colesterol sérico
# Colesterol sérico
fig = px.histogram(df.sort_values('class', ascending=True), x="chol", color="class", opacity=0.7, histnorm='probability')
fig.update_layout(title='Distribución del colesterol sérico en pacientes por categoría', barmode='stack')
fig.show()
# Frecuencia máxima alcanzada
fig = px.histogram(df.sort_values('class', ascending=True), x="thalach", color="class", opacity=0.7, histnorm='probability')
fig.update_layout(title='Distribución de la frecuencia máxima en pacientes por categoría', barmode='stack')
fig.show()
Frecuencia máxima alcanzada
# Frecuencia máxima alcanzada
fig = px.histogram(df.sort_values('class', ascending=True), x="oldpeak", color="class", opacity=0.7, histnorm='probability')
fig.update_layout(title='Distribución ST depression por el ejercicio por categoría', barmode='stack')
fig.show()
# Genero de usuario por clase
df_sex = gen_class_plot(df, var='sex', var_type=var_sex).sort_values('class',ascending=False)
fig = px.bar(df_sex, x='sex', y='class', color='idx_class', title='Género de pacientes por categoría')
fig.show()
# Tipos de dolor en el pecho
df_cp = gen_class_plot(df, var='cp', var_type=var_cp)
fig = px.bar(df_cp, x='class', y='cp', color='idx_class', title='Pacientes con dolor en el pecho por categoría')
fig.show()
Con base en los resultados anteriores:
Typical Angina.# Azúcar en la sangre en ayunas
df_fbs = gen_class_plot(df, var='fbs', var_type=var_fbs)
fig = px.bar(df_fbs, x='class', y='fbs', color='idx_class', title='Azúcar en pacientes por categoría')
fig.show()
La imagen anterior muestra que el 85% de los valores es True. Por tanto, es posible que no sea una variable discrepante.
# resultados electrográficos en repososo
df_restecg = gen_class_plot(df, var='restecg', var_type=var_restecg).sort_values('class',ascending=True)
fig = px.bar(df_restecg, x='class', y='restecg', color='idx_class', title='Resultados electrográficos en reposo por categoría')
fig.show()
Se puede evidenciar según los datos anteriores:
normales y con hipertrofia ventricular izquierda, sin embargo el grupo ST-T wave abnormal tienen una cantidad de pacientes muy baja y por cada grupo, esto tal vez no genera suficiente información para determinar las categorías de los pacientes dado que tienen la misma cantidad de usuarios en cada grupo (1).# Angina inducida por el ejercicio
df_exang = gen_class_plot(df, var='exang', var_type=var_exang).sort_values('class',ascending=True)
fig = px.bar(df_exang, x='class', y='exang', color='idx_class', title='Angina inducida por ejercicio por categoría')
fig.show()
El gráfico anterior nos permite observar:
no y aunque visualmente se vea similar, las proporciones difieren bastante.# Inclinación del pico de segmento ST en ejercicio
df_slope = gen_class_plot(df, var='slope', var_type=var_slope).sort_values('slope',ascending=False)
fig = px.bar(df_slope, x='class', y='slope', color='idx_class', title='Slope en pacientes por categoría')
fig.show()
Los pacientes en su variable slope, cómo en los defectos tienen un comportamiento similar entre grupos, sin embargo se observa:
unsloping y flat tienen una distribución similar entre ellos, la diferencia es que el grupo 3 tiene más pacientes en el grupo flat que en el otro grupo.downsloping son los que menos participación poseen, con 21 pacientes.# Tipo de defecto
df_thal = gen_class_plot(df, var='thal', var_type=var_thal).sort_values('class',ascending=False)
fig = px.bar(df_thal, x='class', y='thal', color='idx_class', title='Tipo de defecto en pacientes por categoría')
fig.show()
Según el gráfico anterior se puede concluir:
normal es el que posee la mayor cantidad de pacientes, con cerca de 160, adicional a eso 127 pacientes de este grupo son pertenecientesl al grupo 0.`fijo son los que presentan una menor participación.# Sunburst para entendimiento de tipos de dolor de pecho por clase y género
fig = px.sunburst(data_frame = df.replace({'sex':var_sex, 'cp':var_cp}),
path = [ 'sex','class','cp'],
color = 'class',
maxdepth = -1,
title = 'Gráfico Sunburst > Gender > Age')
fig.update_traces(textinfo = 'label+percent parent')
fig.update_layout(margin=dict(t=0, l=0, r=0, b=0))
fig.show()
El gráfico nos permite analizar:
# Gráfico de correlación para entender posibles relaciones
fig = px.imshow(df.corr(), title='Matriz de correlaciones sobre variables')
fig.update_xaxes(side="top")
fig.show()
A continuación se empezará con el proceso de preparación de datos que van a ser utilizados para entrenar y validar el modelo.
# renombrar variable target por `label`
X = df.copy(deep=True)
X = X.rename(columns={'class': 'label'})
Y = X.pop('label')
# Creación de un kbins discretizer para aplicar a la edad y conformarla por grupos
kbins = KBinsDiscretizer(n_bins=7,encode='ordinal',strategy='kmeans')
age = pd.DataFrame(X.pop('age'))
X['age'] = kbins.fit_transform(age).astype('int32')
# Estandarización de variables continuas
standardScaler = StandardScaler()
cols_to_standarize = X.select_dtypes(['int16', 'float16']).columns
X[cols_to_standarize] = standardScaler.fit_transform(X[cols_to_standarize])
# Binning de variables categóricas
X = pd.get_dummies(X, columns=['sex', 'cp', 'fbs', 'restecg', 'exang', 'slope', 'ca', 'thal'] )
X.describe()
| trestbps | chol | thalach | oldpeak | age | sex_0 | sex_1 | cp_1 | cp_2 | cp_3 | cp_4 | fbs_0 | fbs_1 | restecg_0 | restecg_1 | restecg_2 | exang_0 | exang_1 | slope_1 | slope_2 | slope_3 | ca_0 | ca_1 | ca_2 | ca_3 | thal_3 | thal_6 | thal_7 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 2.970000e+02 | 2.970000e+02 | 2.970000e+02 | 2.970000e+02 | 297.000000 | 297.000000 | 297.000000 | 297.000000 | 297.000000 | 297.000000 | 297.000000 | 297.000000 | 297.000000 | 297.000000 | 297.000000 | 297.000000 | 297.000000 | 297.000000 | 297.000000 | 297.000000 | 297.000000 | 297.000000 | 297.000000 | 297.000000 | 297.000000 | 297.000000 | 297.000000 | 297.000000 |
| mean | -1.814982e-08 | -4.462195e-09 | -4.339900e-09 | 1.708365e-08 | 3.259259 | 0.323232 | 0.676768 | 0.077441 | 0.164983 | 0.279461 | 0.478114 | 0.855219 | 0.144781 | 0.494949 | 0.013468 | 0.491582 | 0.673401 | 0.326599 | 0.468013 | 0.461279 | 0.070707 | 0.585859 | 0.218855 | 0.127946 | 0.067340 | 0.552189 | 0.060606 | 0.387205 |
| std | 1.001688e+00 | 1.001688e+00 | 1.001688e+00 | 1.001688e+00 | 1.448481 | 0.468500 | 0.468500 | 0.267741 | 0.371792 | 0.449492 | 0.500364 | 0.352474 | 0.352474 | 0.500818 | 0.115462 | 0.500773 | 0.469761 | 0.469761 | 0.499818 | 0.499340 | 0.256768 | 0.493404 | 0.414168 | 0.334594 | 0.251033 | 0.498108 | 0.239009 | 0.487933 |
| min | -2.125634e+00 | -2.337704e+00 | -3.431849e+00 | -9.067173e-01 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
| 25% | -6.594306e-01 | -7.002541e-01 | -7.247694e-01 | -9.067173e-01 | 2.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 1.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
| 50% | -9.550636e-02 | -8.380217e-02 | 1.484822e-01 | -2.196856e-01 | 3.000000 | 0.000000 | 1.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 1.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 1.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 1.000000 | 0.000000 | 0.000000 | 0.000000 | 1.000000 | 0.000000 | 0.000000 |
| 75% | 4.684179e-01 | 5.519139e-01 | 7.160957e-01 | 4.673461e-01 | 4.000000 | 1.000000 | 1.000000 | 0.000000 | 0.000000 | 1.000000 | 1.000000 | 1.000000 | 0.000000 | 1.000000 | 0.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 0.000000 | 1.000000 | 0.000000 | 0.000000 | 0.000000 | 1.000000 | 0.000000 | 1.000000 |
| max | 3.851964e+00 | 6.099981e+00 | 2.287949e+00 | 4.418408e+00 | 6.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 |
X.info()
<class 'pandas.core.frame.DataFrame'> Int64Index: 297 entries, 0 to 301 Data columns (total 28 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 trestbps 297 non-null float64 1 chol 297 non-null float64 2 thalach 297 non-null float64 3 oldpeak 297 non-null float64 4 age 297 non-null int32 5 sex_0 297 non-null uint8 6 sex_1 297 non-null uint8 7 cp_1 297 non-null uint8 8 cp_2 297 non-null uint8 9 cp_3 297 non-null uint8 10 cp_4 297 non-null uint8 11 fbs_0 297 non-null uint8 12 fbs_1 297 non-null uint8 13 restecg_0 297 non-null uint8 14 restecg_1 297 non-null uint8 15 restecg_2 297 non-null uint8 16 exang_0 297 non-null uint8 17 exang_1 297 non-null uint8 18 slope_1 297 non-null uint8 19 slope_2 297 non-null uint8 20 slope_3 297 non-null uint8 21 ca_0 297 non-null uint8 22 ca_1 297 non-null uint8 23 ca_2 297 non-null uint8 24 ca_3 297 non-null uint8 25 thal_3 297 non-null uint8 26 thal_6 297 non-null uint8 27 thal_7 297 non-null uint8 dtypes: float64(4), int32(1), uint8(23) memory usage: 19.4 KB
X.head()
| trestbps | chol | thalach | oldpeak | age | sex_0 | sex_1 | cp_1 | cp_2 | cp_3 | cp_4 | fbs_0 | fbs_1 | restecg_0 | restecg_1 | restecg_2 | exang_0 | exang_1 | slope_1 | slope_2 | slope_3 | ca_0 | ca_1 | ca_2 | ca_3 | thal_3 | thal_6 | thal_7 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.750380 | -0.276443 | 0.017494 | 1.069652 | 5 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
| 1 | 1.596266 | 0.744555 | -1.816334 | 0.381782 | 5 | 0 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |
| 2 | -0.659431 | -0.353500 | -0.899420 | 1.326346 | 5 | 0 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 |
| 3 | -0.095506 | 0.051047 | 1.633010 | 2.099781 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 0 | 0 |
| 4 | -0.095506 | -0.835103 | 0.978071 | 0.296217 | 1 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 |
# Creación del train-test-split para la fase de entrenamiento
X_train, X_test, y_train, y_test = train_test_split(X.values, Y.values, test_size=0.20, random_state=42, stratify=Y)
print(f"the shape of X_train: {X_train.shape}")
print(f"the shape of X_test: {X_test.shape}")
print(f"the shape of y_train: {y_train.shape}")
print(f"the shape of y_test: {y_test.shape}")
the shape of X_train: (237, 28) the shape of X_test: (60, 28) the shape of y_train: (237,) the shape of y_test: (60,)
# Configuración de los hiperparámetros a optimizar
ada_search = {
'model': [AdaBoostClassifier()],
'model__learning_rate': Real(0.0005, 0.9, prior="log-uniform"),
'model__n_estimators': Integer(1, 1000),
'model__algorithm': Categorical(['SAMME', 'SAMME.R'])
}
gb_search = {
'model': [GradientBoostingClassifier()],
'model__learning_rate': Real(0.0005, 0.9, prior="log-uniform"),
'model__n_estimators': Integer(1, 1000),
'model__criterion': Categorical(['friedman_mse', 'mse', 'mae'])
}
# Definición del pipeline para entrenar
pipe = Pipeline([
('model', GradientBoostingClassifier())
])
# Definicición del modelo, con cross-validation (cv=5)
opt = BayesSearchCV(
pipe,
[(ada_search, 100), (gb_search, 100)],
cv=3,
random_state=42
)
# Entrenamiento del modelo
opt.fit(X_train, y_train)
BayesSearchCV(cv=3, error_score='raise',
estimator=Pipeline(memory=None,
steps=[('model',
GradientBoostingClassifier(ccp_alpha=0.0,
criterion='friedman_mse',
init=None,
learning_rate=0.1,
loss='deviance',
max_depth=3,
max_features=None,
max_leaf_nodes=None,
min_impurity_decrease=0.0,
min_impurity_split=None,
min_samples_leaf=1,
min_samples_split=2,
min_weight_frac...
subsample=1.0,
tol=0.0001,
validation_fraction=0.1,
verbose=0,
warm_start=False)],
'model__criterion': Categorical(categories=('friedman_mse', 'mse', 'mae'), prior=None),
'model__learning_rate': Real(low=0.0005, high=0.9, prior='log-uniform', transform='identity'),
'model__n_estimators': Integer(low=1, high=1000, prior='uniform', transform='identity')},
100)],
verbose=0)
# Validación de los valores obtenidos después del entrenamiento
print(f"validation score: {opt.best_score_}")
print(f"test score: {opt.score(X_test, y_test)}")
print(f"best parameters: {str(opt.best_params_)}")
validation score: 0.6075949367088608
test score: 0.6
best parameters: OrderedDict([('model', AdaBoostClassifier(algorithm='SAMME', base_estimator=None,
learning_rate=0.016947399831088224, n_estimators=840,
random_state=None)), ('model__algorithm', 'SAMME'), ('model__learning_rate', 0.016947399831088224), ('model__n_estimators', 840)])
# Creación de las variables sobre train a predecir
Y_train_opt = opt.predict(X_train)
Y_train_opt.shape
# Creación de las variables sobre test a predecir
Y_pred_opt = opt.predict(X_test)
Y_pred_opt.shape
(60,)
# Reporte de clasificación obtenido en la data de train
print(classification_report(y_train,Y_train_opt))
precision recall f1-score support
0 0.77 0.98 0.86 128
1 0.36 0.37 0.37 43
2 0.40 0.29 0.33 28
3 0.60 0.21 0.32 28
4 1.00 0.10 0.18 10
accuracy 0.66 237
macro avg 0.63 0.39 0.41 237
weighted avg 0.64 0.66 0.62 237
# Reporte de clasificación obtenido en la data de test
print(classification_report(y_test,Y_pred_opt))
precision recall f1-score support
0 0.82 1.00 0.90 32
1 0.21 0.27 0.24 11
2 0.20 0.14 0.17 7
3 0.00 0.00 0.00 7
4 0.00 0.00 0.00 3
accuracy 0.60 60
macro avg 0.25 0.28 0.26 60
weighted avg 0.50 0.60 0.54 60
# Matriz de confusión de los valores
confusion_matrix(y_test, Y_pred_opt)
array([[30, 1, 0, 1, 0],
[ 5, 5, 1, 0, 0],
[ 0, 5, 0, 2, 0],
[ 0, 3, 3, 1, 0],
[ 0, 1, 0, 2, 0]])
* Como se puede observar al comprar las métricas del set de entrenamiento y de test. Se puede evidenciar que el modelo no esta generalizando lo suficiente. Ya que la mayoría de los datos los clasifica como “0”. Esto en gran parte se debe al sesgo de los datos, debido a que gran parte de la muestra es de clase “0”. Para mejorar dicha medición se podría intentar balancear las clases por medio de la eliminación de datos de la clase 0. Sin embargo, hay que considerar que esta estrategia puede disminuir en gran medida la cantidad de datos totales para el entrenamiento.
* A nivel de accuracy, se observa que en la data de `train` y `test` está relativamente cerca, con lo cual se evidencia que el modelo está generalizando lo suficiente.
* A nivel de `precision` y `recall` en los grupos 3 y 4 se evidencia que poseen valores de 0, mostrando la incapacidad del modelo para clasificar correctamente en estos grupos.
* El mayor problema evidenciado es la falta de un conjunto de datos mayor para las categorías diferente de 0, el desbalanceo del conjunto de datos hace que las predicciones sobre este sean poco efectivas, por lo cual se sugiere añadir nuevos datos para mejorar el performance del modelo.
* Inicialmente se corrió un modelo con la variable edad sin discretizar, pero se evidencia que el modelo tiene un peor performance, por lo cual finalmente se opta por la estrategía de Kbinning a fin de crear rangos de edad óptimos.
* También se realiza una ejecución del modelo sin generar variables dummy a las categorías, sin embargo se evidencia que esto no ayuda al modelo a la hora de predecir, por tal motivo se aplica este método a las variables categóricas y se deja este conjunto final de datos para entrenar y validar.